package view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;


/**
 * An abstract class for data visualizations. It sets basic visualization
 * properties and specifies how data should be passed to the visualization.
 * Visualizations are return as JComponents.
 * 
 * @author Grant Oakley
 *         edited by Zachary Hopping
 * 
 */
@SuppressWarnings("serial")
public abstract class Visualization extends JComponent {

    /**
     * Constant for referencing the x axis of a plot.
     */
    public static final int X_AXIS = 0;
    /**
     * Constant for referencing the y axis of a plot.
     */
    public static final int Y_AXIS = 1;
    /**
     * Constant for referencing the z axis of a plot.
     */
    public static final int Z_AXIS = 2;

    private static final float PLOT_BOUNDARY_LEFT = .1f;
    private static final float PLOT_BOUNDARY_RIGHT = .9f;
    private static final float PLOT_BOUNDARY_TOP = .1f;
    private static final float PLOT_BOUNDARY_BOTTOM = .9f;

    private static final float X_AXIS_OVERHANG = .01f;
    private static final float Y_AXIS_OVERHANG = .01f;
    private static final float AXIS_THICKNESS = 3f;

    private static final int DEFAULT_WIDTH = 700;
    private static final int DEFAULT_HEIGHT = 500;

    private static final int MINIMUM_WIDTH = 200;
    private static final int MINIMUM_HEIGHT = 200;

    private static final String INOPERABLE_NUMBER_OF_VECTORS_ERROR_MESSAGE =
            "Inoperable number of vectors provided";
    private static final String AXIS_DOES_NOT_EXIST = "Specified axis does not exist.";

    private String myTitle = "";
    private Color myColor = Color.BLACK;
    private List<DataVector<? extends Comparable<?>>> myAxes;

    /**
     * Default constructor without size specification.
     */
    public Visualization () {
        this(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));
    }

    /**
     * Calls the constructor of JComponent.
     * 
     * @param preferredSize dimensions the visualize should default to
     */
    public Visualization (Dimension preferredSize) {
        super();
        setPreferredSize(preferredSize);
        setMinimumSize(new Dimension(MINIMUM_WIDTH, MINIMUM_HEIGHT));
        myAxes = new ArrayList<DataVector<? extends Comparable<?>>>();
    }

    /**
     * Adds the data vectors that are to be visualized. If the number of data
     * vectors is not appropriate for the visualization type, a
     * WrongNumberOfDataVectorsException will be thrown.
     * 
     * @param data the vector(s) of data that are to be visualized
     */
    public void addDataVectors (List<? extends Comparable<?>> ... data) {
        if (isCorrectNumberOfDataVectors(data.length) && dataVectorDimensionAgreement(data)) {
            for (int i = 0; i < data.length; i++) {
                myAxes.add(i, new DataVector(data[i]));
            }
        }
        else {
            throw new ViewException(INOPERABLE_NUMBER_OF_VECTORS_ERROR_MESSAGE, data.length);
        }
    }

    /**
     * Removes any data that have been added using the addDataVectors method.
     */
    public void clearData () {
        myAxes.clear();
    }

    @Override
    public abstract void paint (Graphics g);

    /**
     * Creates a text label for the axis number specified.
     * 
     * @param axisNumber number of the axis to be labeled (e.g. the x axis of a
     *        scatter plot would have axisNumber 0)
     * @param label name of the access to paint on the plot
     */
    public void setAxisLabel (int axisNumber, String label) {
        myAxes.get(axisNumber).setLabel(label);
    }

    /**
     * Specifies the range of data to be displayed along the specified axis.
     * 
     * @param axisNumber which axis to apply the range to
     * @param lowerBound the lowest value to be plotted
     * @param upperBound the highest value to be plotted
     */
    public void setAxisRange (int axisNumber, float lowerBound, float upperBound) {
        try {
            myAxes.get(axisNumber).setRangeLowerBound(lowerBound);
            myAxes.get(axisNumber).setRangeUpperBound(upperBound);
        }
        catch (IndexOutOfBoundsException e) {
            throw new ViewException(AXIS_DOES_NOT_EXIST, axisNumber);
        }
    }

    /**
     * Sets the primary color of the visualization.
     * 
     * @param c new color of the visualization
     */
    public void setColor (Color c) {
        myColor = c;
    }

    /**
     * Get primary color of the visualization.
     * 
     * @return primary color of the visualization
     */
    public Color getColor () {
        return myColor;
    }

    /**
     * Sets the title to be shown at the top of the visualization. None by
     * default.
     * 
     * @param title text at the top of the visualization
     */
    public void setTitle (String title) {
        myTitle = title;
    }

    /**
     * Gets the text value of the visualization's title.
     * 
     * @return text String reference of the visualization's title
     */
    public String getTitle () {
        return myTitle;
    }

    /**
     * Checks if the number of data vectors can be supported by this
     * visualization.
     * 
     * @param numberOfVectors number of vectors to be plotted
     * @return true if this number of vectors can be plotted
     */
    public abstract boolean isCorrectNumberOfDataVectors (int numberOfVectors);

    protected abstract boolean dataVectorDimensionAgreement (List<?>[] vectors);

    /**
     * Paints the axis labels and numbers them. This is not called by default in
     * the paint method in case a subclass is making a visualization for which
     * an x-y axis does not make sense (e.g. a 3D visualization or a pie chart).
     * 
     * @param g2d
     */
    protected void paint2DAxisAndLabels (Graphics2D g2d) {
        int visualizationWidth = getSize().width;
        int visualizationHeight = getSize().height;

        int left = (int) (visualizationWidth * PLOT_BOUNDARY_LEFT);
        int right = (int) (visualizationWidth * PLOT_BOUNDARY_RIGHT);
        int top = (int) (visualizationHeight * PLOT_BOUNDARY_TOP);
        int bottom = (int) (visualizationHeight * PLOT_BOUNDARY_BOTTOM);
        int axisRight = (int) (visualizationWidth * (PLOT_BOUNDARY_RIGHT + X_AXIS_OVERHANG));
        int axisTop = (int) (visualizationHeight * (PLOT_BOUNDARY_TOP - Y_AXIS_OVERHANG));

        // Cover any erroneous off graph marks by a Visualization subclass
        g2d.setColor(Color.WHITE);
        g2d.fillRect(left, 0, right - left, top);
        g2d.fillRect(left, bottom, right - left, getSize().height - bottom);
        g2d.fillRect(0, 0, left, getSize().height);
        g2d.fillRect(right, 0, getSize().width - right, getSize().height);
        // Paint axes
        g2d.setColor(Color.BLACK);
        Stroke storedStroke = g2d.getStroke();
        g2d.setStroke(new BasicStroke(AXIS_THICKNESS));
        g2d.drawLine(left, bottom, axisRight, bottom);
        g2d.drawLine(left, axisTop, left, bottom);
        g2d.setStroke(storedStroke);
        paint2DAxisLabels(g2d, left, right, top, bottom);
        paint2DAxisNumbering(g2d, left, right, top, bottom);
    }

    private void paint2DAxisLabels (Graphics2D g2d, int left, int right, int top, int bottom) {
        String xLabel = getAxisLabel(X_AXIS);
        String yLabel = getAxisLabel(Y_AXIS);

        FontMetrics fm = g2d.getFontMetrics();
        int fontHeight = fm.getHeight();
        int titleWidth = fm.stringWidth(getTitle());
        int xLabelWidth = fm.stringWidth(xLabel);
        int yLabelWidth = fm.stringWidth(yLabel);

        g2d.drawString(getTitle(), ((left + right) / 2) - (titleWidth / 2), top - fontHeight);
        g2d.drawString(xLabel, ((left + right) / 2) - (xLabelWidth / 2), bottom + 2 * fontHeight);
        g2d.translate(left - 2 * fontHeight, ((top + bottom) / 2) - (yLabelWidth / 2));
        g2d.rotate(Math.PI / 2);
        g2d.drawString(yLabel, 0, 0);
        g2d.rotate(-Math.PI / 2);
        g2d.translate(-(left - 2 * fontHeight), -(((top + bottom) / 2) - (yLabelWidth / 2)));
    }

    private String getAxisLabel (int axisNumber) {
        String label = "";
        if (getAxis(axisNumber) != null) {
            label = myAxes.get(axisNumber).getLabel();
        }
        return label;
    }

    private void paint2DAxisNumbering (Graphics2D g2d, int left, int right, int top, int bottom) {
        FontMetrics fm = g2d.getFontMetrics();
        int fontHeight = fm.getHeight();
        int maximumLabelWidth;
        if (getAxis(X_AXIS) != null) {
            DataVector axis = getAxis(X_AXIS);
            String rangeMaximum = axis.getLargestElementInRangeAsString();
            maximumLabelWidth = fm.stringWidth(rangeMaximum);
            g2d.drawString(rangeMaximum, left, bottom + fontHeight);
            g2d.drawString(axis.getSmallestElementInRangeAsString(), right - maximumLabelWidth,
                           bottom + fontHeight);
        }
        if (getAxis(Y_AXIS) != null) {
            DataVector axis = getAxis(Y_AXIS);
            String rangeMaximum = axis.getLargestElementInRangeAsString();
            maximumLabelWidth = fm.stringWidth(rangeMaximum);
            g2d.translate(left - fontHeight, top);
            g2d.rotate(Math.PI / 2);
            g2d.drawString(axis.getSmallestElementInRangeAsString(), 0, 0);
            g2d.drawString(rangeMaximum, bottom - top - maximumLabelWidth, 0);
            g2d.rotate(-Math.PI / 2);
            g2d.translate(-(left - fontHeight), -top);
        }
    }

    protected float mapToRange (float value, float valueMin, float valueMax, float destinationMin,
                                float destinationMax) {
        return ((value - valueMin) / (valueMax - valueMin)) * (destinationMax - destinationMin) +
               destinationMin;
    }

    protected DataVector<? extends Comparable<?>> getAxis (int axisNumber) {
        if (axisNumber < myAxes.size() && axisNumber >= 0) {
            return myAxes.get(axisNumber);
        }
        else {
            return null;
        }
    }

    protected float getPlotBoundaryLeft () {
        return PLOT_BOUNDARY_LEFT;
    }

    protected float getPlotBoundaryRight () {
        return PLOT_BOUNDARY_RIGHT;
    }

    protected float getPlotBoundaryTop () {
        return PLOT_BOUNDARY_TOP;
    }

    protected float getPlotBoundaryBottom () {
        return PLOT_BOUNDARY_BOTTOM;
    }
}
